package com.vivi.codereview.actors;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import com.vivi.codereview.WhatTheFuckException;
import com.vivi.codereview.tools.Log;
import com.vivi.codereview.tools.MavenProject;
import com.vivi.codereview.tools.XmlReviewHandler;

/**
 * The developer actor considering AgileReview plugin. We base logic on xml
 * files generated by AgileReview to have list of impacted files.
 *
 * @author vivi
 *
 */
public class AgileDeveloperActor extends DeveloperActor {

	/**
	 * Regexp to find review files.
	 */
	private static final String REVIEW_FILE = "author_(.+).xml";

	/**
	 * Name of a maven pom file.
	 */
	private static final String POM_FILE = "pom.xml";

	/**
	 * Array of all with code folders. Since we reach one of those folders, we stop looking pom files into.
	 */
	private static final String[] CODE_FOLDERS = new String[] { "src", "target", "target-eclipse", ".settings",
	        ".metadata" };

	/**
	 * Handler for XML processes.
	 */
	private XmlReviewHandler xmlUtil;

	/**
	 * Constructor.
	 */
	public AgileDeveloperActor() {
		super();
		this.xmlUtil = new XmlReviewHandler();
	}

	@Override
	public int process() throws Exception {
		Log.logSeparator();
		// We look for jdevcc folder
		String jdevccFolder = getRootDirectory();
		// we get the folder (stream) we want to use
		String rootStream = selectFolder(jdevccFolder,
		        "Please select index of stream you want to use in list above (mandatory step):");
		// We get the right review project to know which files have to be
		// considered
		String reviewFolder = selectFolder(REVIEW_FOLDER,
		        "Please select the Review project you want to use (mandatory step):");
		Map<MavenProject, String> mavenMappings = getMavenProjectsMapping(rootStream);
		Map<String, Set<String>> parsedFiles = getReviewedFiles(reviewFolder);
		// we create a zip file with selected files
		Set<String> chosenFilenames = adaptMavenProjects(parsedFiles, mavenMappings);
		if (checkToBeZipped(chosenFilenames)) {
      if (checkJunitInFiles(chosenFilenames)) {
        String reviewFolderCanonicalName = new File(reviewFolder).getName();
        String subZipFolder = getSysUtil().getFullFilename(reviewFolder, SUB_ZIP_FOLDER);
        createZipFolder(subZipFolder);
        getZipUtil().createZipFile(rootStream, reviewFolderCanonicalName, chosenFilenames,
            getSysUtil().getFullFilename(subZipFolder, ZIP_FILENAME));
      }
		  else {
		    Log.log("No JUnit found in project, zip file not generated!!!", Log.W);
		    throw new WhatTheFuckException(Log.Exit.NO_JUNIT_EXIT);
		  }
		}
		return Log.Exit.OK;
	}

	/**
	 * Creates the sub zip folder in review folder if needed.
	 * @param subZipFolder
	 */
	private void createZipFolder(String subZipFolder) throws Exception {
	  new File(subZipFolder).mkdirs();
	}

	/**
	 * Since we have on one side the details of each pom, and on the other side,
	 * the project name coming from eclipse, we need to do correlation logic to
	 * get the full path of each file
	 *
	 * @param parsedFiles
	 *            the files we want to consider, the path starting at maven
	 *            module level
	 * @param mavenMappings
	 *            the Mapping between each maven module and its absolute path
	 * @return Set of files, the ones we consider in parsedFiles, with absolute
	 *         path
	 * @throws Exception
	 */
	private Set<String> adaptMavenProjects(Map<String, Set<String>> parsedFiles, Map<MavenProject, String> mavenMappings)
	        throws Exception {
		Log.log("We are replacing the maven project by absolute path.", Log.D);
		Set<String> absoluteFiles = new TreeSet<String>();
		for (Map.Entry<String, Set<String>> entry : parsedFiles.entrySet()) {
			String combinedProject = entry.getKey();
			Log.log("working on project " + combinedProject, Log.D);
			String mvnProjectPath = lookForMavenProject(combinedProject, mavenMappings);
			for (String file : entry.getValue()) {
				String finalFile = mvnProjectPath + file;
				Log.log("We consider " + finalFile, Log.I);
				absoluteFiles.add(finalFile);
			}
		}
		return absoluteFiles;
	}

	/**
	 * Look, for a given maven project name in Eclipse, the associated maven
	 * project, then we return the absolute path
	 *
	 * @param combinedProject
	 *            the maven project name, in Eclipse format
	 * @param mavenMappings
	 *            the Mapping between each maven module and its absolute path
	 * @return the absolute path of the selected maven project
	 * @throws Exception
	 */
	private String lookForMavenProject(String combinedProject, Map<MavenProject, String> mavenMappings)
	        throws Exception {
		for (Map.Entry<MavenProject, String> entry : mavenMappings.entrySet()) {
			if (combinedProject.equalsIgnoreCase(entry.getKey().getFormatted())) {
				Log.log("We found the maven link between " + combinedProject + " and " + entry.getValue(), Log.D);
				return entry.getValue();
			}
		}
		Log.log("We are not able to find maven pom related to this project "+combinedProject+". Has to follow this naming [groupId].[artifactId].", Log.E);
		throw new WhatTheFuckException(Log.Exit.MVN_PROJECT_NOT_FOUND);
	}

	/**
	 * We get all the files integrated in the reviews files, sorted by maven
	 * project.
	 *
	 * @param reviewFolder
	 *            the folder with the AgileReview project
	 * @return Map with maven project (Eclipse format) as key, and Set of files
	 *         as value
	 * @throws Exception
	 */
	private Map<String, Set<String>> getReviewedFiles(String reviewFolder) throws Exception {
		Map<String, Set<String>> selectedFiles = new HashMap<String, Set<String>>();
		for (File file : new File(reviewFolder).listFiles()) {
			Log.log("We are considering this file " + file, Log.D);
			if (file.getName().matches(REVIEW_FILE)) {
				try {
					Document document = getSysUtil().readXmlFile(file);
					Log.log("We will parse " + file, Log.D);
					Map<String, Set<String>> readFiles = readFilesFromXml(document);
					mergeMaps(selectedFiles, readFiles);
				} catch (IOException e) {
					Log.log(file + " cannot be read. Will be ignored.", Log.W);
					throw new WhatTheFuckException(Log.Exit.IO_EXCEPTION);
				} catch (ParserConfigurationException e) {
					Log.log(file + " cannot be parsed because of wrong configuration. Will be ignored.", Log.W);
					throw new WhatTheFuckException(Log.Exit.XML_PARSING_EXCEPTION);
				} catch (SAXException e) {
					Log.log(file + " cannot be parsed because of SAX implementation. Will be ignored.", Log.W);
					throw new WhatTheFuckException(Log.Exit.XML_SAX_EXCEPTION);
				}
			} else {
				Log.log("We are bypassing this file " + file, Log.D);
			}
		}
		return selectedFiles;
	}

	/**
	 * Get the files declared into a XML review file
	 *
	 * @param document
	 *            the XM file, in a Java representation (DOM parsing)
	 * @return Map with maven project (Eclipse format) as key, and Set of files
	 *         as value
	 */
	private Map<String, Set<String>> readFilesFromXml(Document document) {
		Map<String, Set<String>> map = getXmlUtil().parseXmlReviewFile(document);
		Log.log("We finished parsing review file and found " + map.size() + " entries.", Log.D);
		return map;
	}

	/**
	 * @return the xmlUtil
	 */
	protected XmlReviewHandler getXmlUtil() {
		return xmlUtil;
	}

	/**
	 * Read a pom file and return a Java representation of the maven project
	 *
	 * @param pom
	 *            the XML file in Java representation
	 * @param path
	 *            to be loggued in case of issue
	 * @return a mavenProject to represente the project
	 */
	public MavenProject readPom(Document pom, String path) {
		String groupId = null;
		String artifactId = null;
		NodeList mainNodes = pom.getChildNodes();
		int lengthMain = mainNodes.getLength();
		int i = 0;
		while ((groupId != null || artifactId == null) && i < lengthMain) {
			Node mainNode = mainNodes.item(i);
			if (mainNode.getNodeName().equals("project")) {
				NodeList projectNodes = mainNode.getChildNodes();
				int lengthProject = projectNodes.getLength();
				int j = 0;
				while ((groupId != null || artifactId == null) && j < lengthProject) {
					Node projectNode = projectNodes.item(j);
					if (projectNode.getNodeName().equals("groupId")) {
						groupId = projectNode.getTextContent();
					} else if (projectNode.getNodeName().equals("artifactId")) {
						artifactId = projectNode.getTextContent();
					}
					j++;
				}
			}
			i++;
		}
		MavenProject mvnProject = null;
		if (groupId != null && artifactId != null) {
			mvnProject = new MavenProject(groupId, artifactId);
		} else {
			Log.log("Issue while parsing pom " + path + ". We found groupId=" + groupId + " and artifactId="
			        + artifactId, Log.D);
		}
		return mvnProject;
	}

	/**
	 * Main concept of this method is to create the mapping between each Maven
	 * module and the associated absolute path, in a given stream.
	 *
	 * @param stream
	 *            the stream
	 * @return Map between a Maven project and its absolute path
	 */
	public Map<MavenProject, String> getMavenProjectsMapping(String stream) throws Exception {
		Map<MavenProject, String> map = new HashMap<MavenProject, String>();
		Log.log("We are currently parsing " + stream + " to find maven poms to map groupId and artifactId.", Log.D);
		getMavenProjectsMapping(stream, map);
		Log.log("We finished parsing poms and found " + map.size() + " entries.", Log.D);
		return map;
	}

	/**
	 * Private intern and recursive method to parse all maven files in a given
	 * stream
	 *
	 * @param currDir
	 *            the current directory during the parsing
	 * @param currMap
	 *            the current mapping, has to be not null
	 */
	private void getMavenProjectsMapping(String currDir, Map<MavenProject, String> currMap) throws Exception {
		if (currDir != null) {
			File[] files = new File(currDir).listFiles();
			if (files != null && files.length > 0) {
				for (File file : files) {
					Log.log("We check " + file, Log.DD);
					// We check if file is a module pom file
					if (isPomFile(file)) {
						try {
							MavenProject pom = readPom(getSysUtil().readXmlFile(file), file.getAbsolutePath());
							if (pom != null) {
								Log.log("We found mapping between " + file + " and " + pom, Log.D);
								currMap.put(pom, currDir);
							}
						} catch (IOException e) {
							Log.log(file + " cannot be read. Will be ignored.", Log.W);
							throw new WhatTheFuckException(Log.Exit.IO_EXCEPTION);
						} catch (ParserConfigurationException e) {
							Log.log(file + " cannot be parsed because of wrong configuration. Will be ignored.", Log.W);
							throw new WhatTheFuckException(Log.Exit.XML_PARSING_EXCEPTION);
						} catch (SAXException e) {
							Log.log(file + " cannot be parsed because of SAX implementation. Will be ignored.", Log.W);
							throw new WhatTheFuckException(Log.Exit.XML_SAX_EXCEPTION);
						}
					}
					// If is not a src, target, target-eclipse, .settings
					// folder, we go deeper
					else if (file.isDirectory() && !isCodeFolder(file)) {
						getMavenProjectsMapping(file.getAbsolutePath(), currMap);
					}
					// we stop
					else {
						Log.log(file + " is not considered", Log.DD);
					}
				}
			}
		}
	}

	/**
	 * Check if current folder is a folder within a maven module, meaning no pom file can be found into.
	 * @param folder the current folder
	 * @return true if is a code folder, meaning we stop looking for pom files into
	 */
	public boolean isCodeFolder(File folder) {
		return folder.isDirectory() && Arrays.asList(CODE_FOLDERS).contains(folder.getName());
	}

	/**
	 * Check if given file is a pom file. We check only based on naming.
	 * @param file the current file
	 * @return true if is a pom file regarding the name
	 */
	public boolean isPomFile(File file) {
		return file.getName().equalsIgnoreCase(POM_FILE);
	}

	public String getName() {
		return "Developer";
	}
}
